from __future__ import annotations
from typing import TYPE_CHECKING, NamedTuple, Callable, Any
from math import sqrt, nan, inf, isnan
try:
    from scipy import stats # type: ignore
    USING_SCIPY = True
except ModuleNotFoundError:
    from .chi2_table_checker import Chi2_Table_Checker
    chi2_table_checker = Chi2_Table_Checker()
    USING_SCIPY = False
from .base_structure import Item

if TYPE_CHECKING:
    from .data_manager import Data_Assistant
    from .cli import Internal_Analysis_Setting, External_Analysis_Setting
    from .base_structure import Item

Internal_Measurement = NamedTuple('Internal_Measurement', [('function', Callable[[frozenset[Item], int, dict[frozenset ,int], int], float]), ('header', str)])
External_Measurement = NamedTuple('External_Measurement', [('function', Callable[[int, int, int, int], tuple[float, ...]]), ('header', tuple[str, ...])])


def get_chi2(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float]:
    f11 = XaY_frequency
    f01 = Y_frequency - XaY_frequency
    f10 = X_frequency - XaY_frequency
    f00 = data_len - f11 - f01 - f10
    F = [f11, f01, f10, f00]
    p11 = X_frequency * Y_frequency / data_len
    p01 = Y_frequency - p11
    p10 = X_frequency - p11
    p00 = data_len - p11 - p01 - p10
    P = [p11, p01, p10, p00]
    chi2 = 0.0
    for index, value in enumerate(F):
        if P[index] == 0:
            return (inf, ) 
        chi2 += (value - P[index]) ** 2 / P[index]
    return (chi2, )

def get_chi2_with_p(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float, float]:
    chi2 = get_chi2(XaY_frequency, X_frequency, Y_frequency, data_len)[0]
    if USING_SCIPY:
        return (chi2, stats.chi2.sf(chi2, 1))
    else:
        return (chi2, chi2_table_checker.get_p(chi2))

def get_confidence(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float]:
    return (XaY_frequency / X_frequency, )

def get_lift(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float]:
    return ((XaY_frequency * data_len) / (X_frequency * Y_frequency), )

def get_phi(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float]:
    if any((X_frequency == 0, Y_frequency == 0, X_frequency == data_len, Y_frequency == data_len)):
        return (nan, )
    else:
        lift = (XaY_frequency * data_len) / (X_frequency * Y_frequency)
        return ((lift - 1) / sqrt((data_len / Y_frequency - 1) * (data_len / X_frequency -1)), )

def get_support(XaY_frequency: int, X_frequency: int, Y_frequency: int, data_len: int) -> tuple[float]:
    return (XaY_frequency / data_len, )

'measurement name: h_confidence'
def get_h_confidence(pattern: frozenset[Item], frequency: int, frequent_patterns: dict[frozenset[Item], int], data_len: int) -> float:
    max_item_frequency = max([frequent_patterns[frozenset([item])] for item in pattern])
    return frequency / max_item_frequency

def get_support_internal(pattern: frozenset[Item], frequency: int, frequent_patterns: dict[frozenset[Item], int], data_len: int) -> float:
    return frequency / data_len

EXTERNAL_MEASUREMENT_MAP = {
    'support': External_Measurement(get_support, ('支持度', )),
    'confidence': External_Measurement(get_confidence, ('置信度', )),
    'lift': External_Measurement(get_lift, ('提升度', )),
    'phi': External_Measurement(get_phi, ('phi系数', )),
    'chi2': External_Measurement(get_chi2, ('chi2', )),
    'pchi2': External_Measurement(get_chi2_with_p, ('chi2', 'p'))
}

INTERNAL_MEASUREMENT_MAP = {
    'h_confidence': Internal_Measurement(get_h_confidence, '全置信度'),
    'support': Internal_Measurement(get_support_internal, '支持度')
}

def format_pattern(pattern: frozenset[Item]) -> str:
    item_names = [item.item_name for item in pattern]
    return ','.join(item_names)

"""
setting is something like this
h_confidence must work internal!!
internal measurement only include h_confidence and support

Internal_Analysis_Setting: ('measurements_name', list[str]), ('tags', set[str]), ('must_has_tag', bool)
External_Analysis_Setting: ('measurements_name', list[str]), ('tags0', set[str]), ('tags1', set[str]), ('must_has_tag', bool)
"""

class Inernal_Analysis_Function_Wrapper:
    def __init__(self, setting: Internal_Analysis_Setting, data_assistant: Data_Assistant) -> None:
        self.setting = setting
        self.tags = setting.tags
        if (len(self.tags) == 0) or (not setting.must_has_tag):
            self.data_len = data_assistant.data_len
        else:
            self.data_len = min([data_assistant.tag_frequency[tag] for tag in self.tags])
        #temporary
        self.data_len = data_assistant.data_len

    def check_tags(self, pattern: frozenset[Item]) -> bool:
        if len(self.tags) == 0:
            return True
        else:
            for item in pattern:
                if (frozenset(item.item_tags) & self.tags) == frozenset():
                    return False
            return True

    def __call__(self, pattern: frozenset[Item], frequency: int, frequent_patterns: dict[frozenset[Item], int]) -> dict[str, str]:
        result: dict[str, str] = {}
        if self.check_tags(pattern):
            result |= {
                '频繁模式': format_pattern(pattern),
                '项数': str(len(pattern)),
                '频数': str(frequency),
                '总数': str(self.data_len),
                '约束标签': str(sorted(list(self.tags)))[1: -1]
            }
            for measurement_name in self.setting.measurements_name:
                measurement = INTERNAL_MEASUREMENT_MAP[measurement_name]
                result[measurement.header] = f'{measurement.function(pattern, frequency, frequent_patterns, self.data_len):.5f}'
            return result
        else:
            return result

class External_Analysis_Function_Wrapper:
    def __init__(self, setting: External_Analysis_Setting, data_assistant: Data_Assistant) -> None:
        self.setting = setting
        self.tags0 = setting.tags0
        self.tags1 = setting.tags1
        #if (len(self.tags0) == 0 and len(self.tags1) == 0) or (not setting.must_has_tag):
        #    self.data_len = data_assistant.data_len
        #else:
        #    self.data_len = min([data_assistant.tag_frequency[tag] for tag in self.tags0] + [data_assistant.tag_frequency[tag] for tag in self.tags1])
        #temporary
        self.data_len = data_assistant.data_len

    def check_tags(self, X_pattern: frozenset[Item], Y_pattern: frozenset[Item]) -> bool:
        if (len(self.tags0) != 0) and (len(self.tags1) != 0):
            for item in X_pattern:
                if (frozenset(item.item_tags) & self.tags0) == frozenset():
                    return False
            for item in Y_pattern:
                if (frozenset(item.item_tags) & self.tags1) == frozenset():
                    return False
            return True
        elif len(self.tags0) != 0:
            for item in X_pattern:
                if (set(item.item_tags) & self.tags0) == set():
                    return False
            return True
        elif len(self.tags1) != 0:
            for item in Y_pattern:
                if (set(item.item_tags) & self.tags1) == set():
                    return False
            return True
        else:
            return True

    def __call__(self, X_pattern: frozenset[Item], Y_pattern: frozenset[Item], XaY_frequency: int, X_frequency: int, Y_frequency: int) -> dict[str, str]:
        result: dict[str, str] = {}
        if self.check_tags(X_pattern, Y_pattern):
            result |= {
                '前模式': format_pattern(X_pattern),
                '后模式': format_pattern(Y_pattern),
                '前模式项数': str(len(X_pattern)),
                '后模式项数': str(len(Y_pattern)),
                '总项数': str(len(X_pattern) + len(Y_pattern)),
                '频数': str(XaY_frequency),
                '总数': str(self.data_len),
                '前模式约束标签': str(sorted(list(self.tags0)))[1: -1],
                '后模式约束标签': str(sorted(list(self.tags1)))[1: -1]
            }
            for measurement_name in self.setting.measurements_name:
                measurement = EXTERNAL_MEASUREMENT_MAP[measurement_name]
                values = measurement.function(XaY_frequency, X_frequency, Y_frequency, self.data_len)
                for index, value in enumerate(values):
                    result[measurement.header[index]] = f'{value:.5f}'
            return result
        else:
            return result

class Analyser:
    def __init__(self, frequent_patterns: dict[frozenset[Item], int], data_assistant: Data_Assistant,
    internal_analysis_settings: list[Internal_Analysis_Setting], external_analysis_settings: list[External_Analysis_Setting]) -> None:
        self.frequent_patterns = frequent_patterns
        self.data_assistant = data_assistant

        self.internal_programs: list[Inernal_Analysis_Function_Wrapper] = []
        self.external_programs: list[External_Analysis_Function_Wrapper] = []

        self.internal_header = ['频繁模式', '项数', '频数', '总数', '约束标签']
        self.external_header = ['前模式', '后模式', '前模式项数', '后模式项数', '总项数', '频数', '总数', '前模式约束标签', '后模式约束标签']

        self.internal_results: list[list[str]] = []
        self.external_results: list[list[str]] = []

        self.internal_header_index_map: dict[str, int] = {header: index for index, header in enumerate(self.internal_header)}
        internal_header_index = len(self.internal_header)
        for internal_analysis_setting in internal_analysis_settings:
            self.internal_programs.append(Inernal_Analysis_Function_Wrapper(internal_analysis_setting, self.data_assistant))
            for measurement_name in internal_analysis_setting.measurements_name:
                header = INTERNAL_MEASUREMENT_MAP[measurement_name].header
                if header not in self.internal_header_index_map:
                    self.internal_header_index_map[header] = internal_header_index
                    self.internal_header.append(header)
                    internal_header_index += 1
        self.internal_header_len = len(self.internal_header)

        self.external_header_index_map: dict[str, int] = {header: index for index, header in enumerate(self.external_header)}
        external_header_index = len(self.external_header)
        for external_analysis_setting in external_analysis_settings:
            self.external_programs.append(External_Analysis_Function_Wrapper(external_analysis_setting, self.data_assistant))
            for measurement_name in external_analysis_setting.measurements_name:
                for header in EXTERNAL_MEASUREMENT_MAP[measurement_name].header:
                    if header not in self.external_header_index_map:
                        self.external_header_index_map[header] = external_header_index
                        self.external_header.append(header)
                        external_header_index += 1
        self.external_header_len = len(self.external_header)

    def run(self):
        for pattern, frequency in self.frequent_patterns.items():
            for function in self.internal_programs:
                result = function(pattern, frequency, self.frequent_patterns)
                if result != {}:
                    row = ['' for _ in range(self.internal_header_len)]
                    for header, value in result.items():
                        row[self.internal_header_index_map[header]] = value
                    self.internal_results.append(row)
            XaY_pattern = pattern 
            XaY_frequency = frequency
            for X_pattern, X_frequency in self.frequent_patterns.items():
                if X_pattern < XaY_pattern:
                    Y_pattern = XaY_pattern - X_pattern
                    Y_frequency = self.frequent_patterns[Y_pattern]
                    for function in self.external_programs:
                        result =  function(X_pattern, Y_pattern, XaY_frequency, X_frequency, Y_frequency)
                        if result != {}:
                            row = ['' for _ in range(self.external_header_len)]
                            for header, value in result.items():
                                row[self.external_header_index_map[header]] = value
                            self.external_results.append(row)